﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Physis {

    [Serializable] // MARKED SERIALIZABLE FOR STORING IN SESSION STATE SERVER 
    public class Application {

        #region Private Properties 

        [NonSerialized]
        private Data.Caching.CacheManager cacheManager = new Data.Caching.CacheManager ();

        private TimeSpan? cacheExpirationData = null;

        private TimeSpan? cacheExpirationReference = null;


        private Data.DatabaseConfiguration enterpriseDatabaseConfiguration = null;

        [NonSerialized]
        private Data.IDatabase enterpriseDatabase = null;

        private Data.DatabaseConfiguration environmentDatabaseConfiguration = null;

        [NonSerialized]
        private Data.IDatabase environmentDatabase = null;


        private Security.Session session = null;


        private Exception lastException = null;

        [NonSerialized]
        private System.Diagnostics.TraceSwitch traceSwitchGeneral = null;

        [NonSerialized]
        private System.Diagnostics.TraceSwitch traceSwitchSecurity = null;


        // FACTORIES 

        private Security.SecurityFactory securityFactory = null;

        private Enterprise.EnterpriseFactory enterpriseFactory = null;

        private Core.CoreFactory coreFactory = null;

        #endregion 


        #region Public, Protected, and Internal Properties 

        internal Data.Caching.CacheManager CacheManager { get { return cacheManager; } }
        
        internal TimeSpan CacheExpirationData {

            get {

                if (cacheExpirationData.HasValue) { return cacheExpirationData.Value; }

                cacheExpirationData = TimeSpan.Zero;

                try {

                    String configurationValueString = String.Empty;

                    if (System.Configuration.ConfigurationManager.AppSettings.GetValues ("CacheExpirationDataSeconds") != null) {

                        configurationValueString = (((String)System.Configuration.ConfigurationManager.AppSettings.GetValues ("CacheExpirationDataSeconds")[0]));

                    }

                    Int32 configurationValueSeconds;

                    if (Int32.TryParse (configurationValueString, out configurationValueSeconds)) {

                        cacheExpirationData = new TimeSpan (0, 0, configurationValueSeconds);

                    }

                }

                catch { /* DO NOTHING */ }

                return cacheExpirationData.Value;

            }

        }

        internal TimeSpan CacheExpirationReference {

            get {

                if (cacheExpirationReference.HasValue) { return cacheExpirationReference.Value; }

                cacheExpirationReference = TimeSpan.Zero;

                try {

                    String configurationValueString = String.Empty;

                    if (System.Configuration.ConfigurationManager.AppSettings.GetValues ("CacheExpirationReferenceSeconds") != null) {

                        configurationValueString = (((String)System.Configuration.ConfigurationManager.AppSettings.GetValues ("CacheExpirationReferenceSeconds")[0]));

                    }

                    Int32 configurationValueSeconds;

                    if (Int32.TryParse (configurationValueString, out configurationValueSeconds)) {

                        cacheExpirationReference = new TimeSpan (0, 0, configurationValueSeconds);

                    }

                }

                catch { /* DO NOTHING */ }

                return cacheExpirationReference.Value;

            }

        }
        

        protected Data.DatabaseConfiguration EnterpriseDatabaseConfiguration {

            get {

                if (enterpriseDatabaseConfiguration == null) {

                    // THE CONSTRUCTOR IS PROTECTED FROM EXCEPTIONS WHEN READING FROM THE .CONFIG FILE

                    enterpriseDatabaseConfiguration = new Data.DatabaseConfiguration ("Physis.Database.Enterprise.Connection");

                }

                return enterpriseDatabaseConfiguration;

            }

        }

        public Data.IDatabase EnterpriseDatabase {

            get {

                if (enterpriseDatabase == null) {

                    System.Security.Principal.WindowsImpersonationContext impersonationContext = null; // THIS MUST BE ON THE OUTSIDE OF THE TRY TO BE AVAILABLE IN THE FINALLY BLOCK

                    try {

                        if (EnterpriseDatabaseConfiguration.TrustedConnection) {

                            // IF USING TRUSTED CONNECTION, IMPERSONATE THE HOST PRINCIPAL AND NOT THE THREAD PRINCIPAL

                            // IT IS DEBATABLE IF THIS IS STILL NEEDED, SHOULD CHECK HOW IT RUNS UNDER IIS 

                            System.Diagnostics.Trace.WriteLineIf (TraceSwitchGeneral.TraceVerbose, "[Enterprise Database] System Security Principal: " + System.Security.Principal.WindowsIdentity.GetCurrent ().Name);

                            impersonationContext = System.Security.Principal.WindowsIdentity.GetCurrent ().Impersonate ();

                            System.Diagnostics.Trace.WriteLineIf (TraceSwitchGeneral.TraceVerbose, "[Enterprise Database] System Security Principal Impersonated: " + System.Security.Principal.WindowsIdentity.GetCurrent ().Name);

                        }

                        switch (EnterpriseDatabaseConfiguration.DatabaseType) {

                            case Data.Enumerations.DatabaseType.MicrosoftSqlServer: enterpriseDatabase = new Data.MsSqlDatabase (EnterpriseDatabaseConfiguration); break;

                        }

                    }

                    catch (Exception exception) {

                        SetLastException (exception);

                    }

                    finally {

                        if (impersonationContext != null) {

                            impersonationContext.Undo ();

                        }

                    }

                }

                return enterpriseDatabase;

            }

        }

        protected Data.DatabaseConfiguration EnvironmentDatabaseConfiguration {

            get {

                if ((environmentDatabaseConfiguration == null) && (session != null)) {

                    // READ ENVIRONMENT CONFIGURATION BASED ON THE SESSION LOGGED IN

                    Enterprise.Environment environment = Enterprise.Environment (session.EnvironmentId, true);

                    if (environment != null) { environmentDatabaseConfiguration = environment.DatabaseConfiguration; }

                }

                return environmentDatabaseConfiguration;

            }

        }

        public Data.IDatabase EnvironmentDatabase {

            get {

                if (environmentDatabase == null) {

                    System.Security.Principal.WindowsImpersonationContext impersonationContext = null; // THIS MUST BE ON THE OUTSIDE OF THE TRY TO BE AVAILABLE IN THE FINALLY BLOCK

                    try {

                        if (EnvironmentDatabaseConfiguration.TrustedConnection) {

                            // IF USING TRUSTED CONNECTION, IMPERSONATE THE HOST PRINCIPAL AND NOT THE THREAD PRINCIPAL

                            // IT IS DEBATABLE IF THIS IS STILL NEEDED, SHOULD CHECK HOW IT RUNS UNDER IIS 

                            System.Diagnostics.Trace.WriteLineIf (TraceSwitchGeneral.TraceVerbose, "[Environment Database] System Security Principal: " + System.Security.Principal.WindowsIdentity.GetCurrent ().Name);

                            impersonationContext = System.Security.Principal.WindowsIdentity.GetCurrent ().Impersonate ();

                            System.Diagnostics.Trace.WriteLineIf (TraceSwitchGeneral.TraceVerbose, "[Environment Database] System Security Principal Impersonated: " + System.Security.Principal.WindowsIdentity.GetCurrent ().Name);

                        }

                        switch (EnvironmentDatabaseConfiguration.DatabaseType) {

                            case Data.Enumerations.DatabaseType.MicrosoftSqlServer: environmentDatabase = new Data.MsSqlDatabase (EnvironmentDatabaseConfiguration); break;

                        }

                    }

                    catch (Exception exception) {

                        SetLastException (exception);

                    }

                    finally {

                        if (impersonationContext != null) {

                            impersonationContext.Undo ();

                        }

                    }

                }

                return environmentDatabase;

            }

        }


        public Security.Session Session { get { return session; } }


        public Exception LastException { get { return lastException; } }

        public System.Diagnostics.TraceSwitch TraceSwitchGeneral {

            get {

                if (traceSwitchGeneral == null) {

                    traceSwitchGeneral = new System.Diagnostics.TraceSwitch ("General", "General Diagnostics Messages");

                }

                return traceSwitchGeneral;

            }

        }

        public System.Diagnostics.TraceSwitch TraceSwitchSecurity {

            get {

                if (traceSwitchSecurity == null) {

                    traceSwitchSecurity = new System.Diagnostics.TraceSwitch ("Security", "Messages related to the Enterprise and Security");

                }

                return traceSwitchSecurity;

            }

        }


        // FACTORIES 

        public Security.SecurityFactory Security { get { return securityFactory; } }

        public Enterprise.EnterpriseFactory Enterprise { get { return enterpriseFactory; } }

        public Core.CoreFactory Core { get { return coreFactory; } }

        #endregion 


        #region Constructors 

        public Application () {

            // INITIALIZE FACTORIES 

            securityFactory = new Security.SecurityFactory (this);

            enterpriseFactory = new Enterprise.EnterpriseFactory (this);

            coreFactory = new Core.CoreFactory (this);

            return;

        }

        #endregion 


        #region Public Methods - Exception Handling

        public void ClearLastException () {

            lastException = null;

            return;

        }

        public void SetLastException (Exception exception) {

            lastException = exception;

            if (lastException != null) {

                #region Trace Switch General - Error 

                if (TraceSwitchGeneral.TraceError) {

                    //if (session != null) {

                    //    System.Diagnostics.Trace.WriteLine (session.UserAccountId + " [" + session.EnvironmentName + "]: " + session.UserDisplayName);

                    //}

                    System.Diagnostics.Trace.WriteLine("Physis.Core.Application [" + DateTime.Now.ToString () + "] [" + lastException.Source + "] " + lastException.Message);

                    if (lastException.InnerException != null) {

                        System.Diagnostics.Trace.WriteLine ("Physis.Core.Application [" + lastException.InnerException.Source + "] " + lastException.InnerException.Message);

                    }

                    System.Diagnostics.Trace.WriteLine ("** Stack Trace **");

                    foreach (System.Diagnostics.StackFrame currentStackFrame in new System.Diagnostics.StackTrace ().GetFrames ()) {

                        System.Diagnostics.Trace.WriteLine ("    [" + currentStackFrame.GetMethod ().Module.Assembly.FullName + "] " + currentStackFrame.GetMethod ().Name);

                    }

                    System.Diagnostics.Trace.Flush ();

                }

                #endregion

#if DEBUG 
                
                System.Diagnostics.Debug.WriteLine ("Physis.Core.Application [" + DateTime.Now.ToString () + "] [" + lastException.Source + "] " + lastException.Message);

                if (lastException.InnerException != null) {

                    System.Diagnostics.Debug.WriteLine ("Physis.Core.Application [" + lastException.InnerException.Source + "] " + lastException.InnerException.Message);

                }

                System.Diagnostics.Debug.WriteLine ("** Stack Trace **");

                foreach (System.Diagnostics.StackFrame currentStackFrame in new System.Diagnostics.StackTrace ().GetFrames ()) {

                    System.Diagnostics.Debug.WriteLine ("    [" + currentStackFrame.GetMethod ().Module.Assembly.FullName + "] " + currentStackFrame.GetMethod ().Name);

                }

#endif

            } // if (lastException != null) 

            return;

        }

        public void SetLastExceptionQuite (Exception exception) {

            lastException = exception; // NO TRACE LOGGING            

            return;

        }

        #endregion 


        #region Authentication Methods

        public Security.AuthenticationResponse Authenticate (Security.ISecurityProvider provider, System.Security.Principal.IIdentity identity, Int64 environmentId, List<Int64> environmentRestrictions) {

            Security.AuthenticationResponse authenticationResponse;


            // BASE LEVEL AUTHENTICATION WITH THE SECURITY PROVIDER 

            authenticationResponse = provider.Authenticate (identity);


            #region AUTHENTICATE USER WITH PHYSIS ENTERPRISE

            // GET SECURITY AUTHORITY 

            // FUTURE: CHANGE THE SECURITY AUTHORITY TYPE BASED ON PROVIDER TYPE, SINCE WE ONLY HAVE WINDOW INTEGRATED 

            // THIS MIGHT REQUIRE REFACTORING SO THAT YOU HAVE DIFFERENT METHODS WITH AN OUTER ONE GETTING THE APPROPRIATE SECURITY AUTHORITY FIRST 

            Security.Enumerations.SecurityAuthorityType securityAuthorityType;

            Security.SecurityAuthority securityAuthority = null;

            Security.UserAccount userAccount = null;

            Enterprise.Environment environment = null;


            // TRY TO GET THE SECURITY AUTHORITY 

            try {

                securityAuthorityType = Physis.Security.Enumerations.SecurityAuthorityType.WindowsIntegrated;

                securityAuthority = Security.SecurityAuthority (securityAuthorityType, authenticationResponse.DomainName, true);
               

                try {

                    // TRY TO GET THE USER ACCOUNT 

                    userAccount = new Security.UserAccount (this, securityAuthority.Id, authenticationResponse.UserAccountName);

                    authenticationResponse.UserAccountId = userAccount.Id; // PREVIOUS STATEMENT THROWS EXCEPTION WHEN NOT FOUND, THIS IS A SAFE ASSIGNMENT 


                    // ENVIRONMENT VALIDATION WHEN NEEDED: -1 FIND DEFAULT, 0 ENTERPRISE ONLY, ELSE ENVIRONMENT ID

                    // IF ENTERPRISE ONLY PERMISSIONS, NO ENVIRONMENT CONNECTION IS REQUIRED AND IS LEFT NULL

                    authenticationResponse.EnvironmentId = environmentId;

                    if (environmentRestrictions == null) { environmentRestrictions = new List<Int64> (); }

                    if (environmentId == -1) {

                        // TRY TO FIND A DEFAULT ENVIRONMENT FOR THE USER 

                        List<Enterprise.Environment> allowedEnvironments = userAccount.Environments;

                        if (environmentRestrictions.Count > 0) {

                            // IF RESTRICTIONS, REMOVE NON-ALLOWED ENVIRONMENTS 

                            allowedEnvironments = (from currentEnvironment in allowedEnvironments

                                                   where environmentRestrictions.Contains (currentEnvironment.Id)

                                                   select currentEnvironment).ToList ();

                        } 

                        // OTHERWISE, AN EMPTY RESTRICTION LIST MEANS NO RESTRICTIONS 

                        if (allowedEnvironments.Count == 1) {

                            // SINGLE ALLOWED ENVIRONMENT FOUND, DEFAULT TO IT

                            authenticationResponse.EnvironmentId = allowedEnvironments[0].Id;

                        }

                    }


                    switch (authenticationResponse.EnvironmentId) {

                        case -1: // NO DEFAULT ENVIRONMENT FOUND AT THIS POINT, RETURN MUST SELECT ENVIRONMENT 

                            authenticationResponse.IsAuthenticated = false;

                            authenticationResponse.AuthenticationError = Physis.Security.Enumerations.AuthenticationError.MustSelectEnvironment;

                            authenticationResponse.LastException = new Exception ("No default environment found. You must select your environment.");

                            break;

                        case 0: // ENTERPRISE ONLY AUTHENTICATION 

                            session = new Security.Session (this, securityAuthority, userAccount, null);

                            authenticationResponse.IsAuthenticated = true;

                            break;

                        default:

                            environment = Enterprise.Environment (authenticationResponse.EnvironmentId, true);

                            if (environment != null) {

                                session = new Security.Session (this, securityAuthority, userAccount, environment);

                                EnvironmentDatabase.OnDemandOpen ();

                                authenticationResponse.IsAuthenticated = (EnvironmentDatabase.ConnectionState == System.Data.ConnectionState.Open);

                                if (!authenticationResponse.IsAuthenticated) {

                                    authenticationResponse.AuthenticationError = Physis.Security.Enumerations.AuthenticationError.UnableToCreateSession;

                                    authenticationResponse.LastException = new Exception ("Unable to successfully connect to the environment requested.");

                                }

                            }

                            else {

                                authenticationResponse.IsAuthenticated = false;

                                authenticationResponse.AuthenticationError = Physis.Security.Enumerations.AuthenticationError.MustSelectEnvironment;

                                authenticationResponse.LastException = lastException;

                            }

                            break;

                    }


                    if (authenticationResponse.IsAuthenticated) {

                        userAccount.UpdateLastLogin ();

                    }

                }

                catch (Exception exception) {

                    authenticationResponse.IsAuthenticated = false;

                    authenticationResponse.AuthenticationError = Physis.Security.Enumerations.AuthenticationError.InvalidUserOrPassword;

                    authenticationResponse.LastException = exception;

#if DEBUG 

                    System.Diagnostics.Debug.WriteLine ("Physis.Core.Application [" + DateTime.Now.ToString () + "] [" + exception.Source + "] " + exception.Message);

                    if (exception.InnerException != null) {

                        System.Diagnostics.Debug.WriteLine ("Physis.Core.Application [" + exception.InnerException.Source + "] " + exception.InnerException.Message);

                    }

#endif 

                }

            }

            catch (Exception exception) {

                authenticationResponse.IsAuthenticated = false;

                authenticationResponse.AuthenticationError = Physis.Security.Enumerations.AuthenticationError.SecurityAuthorityError;

                authenticationResponse.LastException = exception;

            }

            #endregion 


            if ((authenticationResponse.LastException != null) && ((TraceSwitchSecurity.TraceError) || (traceSwitchGeneral.TraceVerbose))) {

                System.Diagnostics.Trace.WriteLine ("Physis.Core.Application [" + DateTime.Now.ToString () + "] [" + authenticationResponse.LastException.Source + "] " + authenticationResponse.LastException.Message);

                if (authenticationResponse.LastException.InnerException != null) {

                    System.Diagnostics.Trace.WriteLine ("Physis.Core.Application [" + authenticationResponse.LastException.InnerException.Source + "] " + authenticationResponse.LastException.InnerException.Message);

                }

            }

            return authenticationResponse;

        }

        public Security.AuthenticationResponse Authenticate (Int64 environmentId, List<Int64> environmentRestrictions) {

            Security.Providers.WindowsIntegrated provider;

            provider = new Security.Providers.WindowsIntegrated ();

            return Authenticate (provider, System.Threading.Thread.CurrentPrincipal.Identity, environmentId, environmentRestrictions);

        }

        public Security.AuthenticationResponse Authenticate (Int64 environmentId) {

            return Authenticate (environmentId, new List<Int64> ()); 

        }

        public Security.AuthenticationResponse Authenticate (List<Int64> environmentRestrictions) {

            return Authenticate (-1, environmentRestrictions);

        }

        public Security.AuthenticationResponse Authenticate () {

            return Authenticate (-1, new List<Int64> ()); // DEFAULT ENVIRONMENT, USED FOR ENTERPRISE ONLY ACCESS 

        }

        #endregion 


    }

}
